孤舟蓑笠翁,独钓寒江雪

Android -- SystemUI -- QS Tile

概述

本文我们来介绍 SystemUI 下拉面板快捷开关的实现原理。
下拉面板中的 QQS 和 QS 分别可以承载 QS Tile ,进行一些快捷设置的功能。

模块划分

  • TileView:主要负责快捷开关的视图展示。
    • QSTileView:继承自 LinearLayout,实现 QS 视图的抽象类。
    • QSTileViewImpl:继承自 QSTileView,
    • CustomizeTileView:继承自 QSTileViewImpl,编辑页面中用到

UML类图

  • QSTile:主要负责快捷开关的逻辑处理。
    • QSTile
    • QSTileImpl
    • CustomTile:编辑页面中需要用到
    • WifiTile
    • BluetoothTile
    • State
    • BooleanState
    • SignalState

UML类图

  • QSHost
    • QSHost:主要负责快捷开关TileView和QSTile的构建,提供获取QSTile集合的接口,提供了展开和收起下拉面板的接口,是快捷开关对外沟通的桥梁。
    • QSTileHost:
    • QSFactory:
    • QSFactoryImpl:继承自 QSFactory

UML类图

  • QSPanel:承载快捷开关的容器。
    • QuickQSPanel:继承自 QSPanel,显示 QQS 部分快捷开关。
    • QSPanel:继承自 LinearLayout,显示 QS 快捷开关。
    • QSTileLayout
    • TileLayout
    • PagedTileLayout
    • SideLabelTileLayout

UML类图

  • QS:QS面板的顶层容器,主要处理QS面板的展开/收起逻辑。
    • QSContainerImpl:下拉面板快捷开关部分的根布局,包含QQS,QS,QSDetail,QSCustomer 等。添加到 QSFragment。
    • QSFragment:包含 QSContainerImpl 的 QSFragment。
    • QS:主要定义了 QS 面板展开/收起相关的接口

QS Tile 的创建

创建 QSTile,通过初始化 QsTileHost,进行 QsTile 的加载。在 QsTileHost 初始化的时候,增加 Tunable 监听。

1
2
3
4
5
6
7
8
9
QSTileHost()
Settings.Secure.getStringForUser("sysui_qs_tiles") // 从 Secure.QS_TILES 获取QS配置
TunerServiceImpl.addTunable()
QSTileHost.onTuningChanged()
QSTileHost.loadTileSpecs()
QSTileHost.getDefaultSpecs() // 如果 Secure.QS_TILES 没有配置,就从R.string.quick_settings_tiles_default读取
QSTileHost.createTile()
QSFactoryImpl.createTile()
QSFactoryImpl.createTileInternal()

TunerServiceImpl 中注册对字段”sysui_qs_tiles”的系统数据库监听,当编辑模式添加或者删除 Tile 时,会触发重新加载 Tile。

1
2
3
4
5
6
7
8
9
10
11
12
13
// TunerServiceImpl.java
private class Observer extends ContentObserver {
......
@Override
public void onChange(boolean selfChange, java.util.Collection<Uri> uris,
int flags, int userId) {
if (userId == mUserTracker.getUserId()) {
for (Uri u : uris) {
reloadSetting(u);
}
}
}
}

根据配置文件生成对应的 QSTile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// QSFactoryImpl.java
private QSTileImpl createTileInternal(String tileSpec) {
// Stock tiles.
switch (tileSpec) {
case "wifi":
return mWifiTileProvider.get();
case "internet":
return mInternetTileProvider.get();
case "bt":
return mBluetoothTileProvider.get();
......
case "alarm":
return mAlarmTileProvider.get();
case "wallet":
return mQuickAccessWalletTileProvider.get();
}

创建 QSTileView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
QSFragment.onViewCreated()
QSPanelController.init()
OnAttachStateChangeListener.onViewAttachedToWindow()
QSPanelController.onViewAttached()
QSPanelControllerBase.onViewAttached()
QSPanelControllerBase.setTiles()
QSTileHost.getTiles()
QSPanelControllerBase.setTiles()
QSPanelControllerBase.addTile()
QSTileHost.createTileView()
QSFactoryImpl.createTileView()
new QSTileViewImpl()
QSPanel.addTile()
QSTile.addCallback() // 向QSTile注册回调
onStateChanged()
drawTile()
QSTileViewImpl.init() // 初始化点击事件
setOnClickListener()
setOnLongClickListener()
QSTile.refreshState()
TileLayout.addTile() // 添加到 TileLayout

从上面流程可以看出来,QSTileHost 对象在构建的时候就通过 QSFactoryImpl 创建好了各个开关的对应的 QSTile,而后 QSPanel 在初始化的过程中,再次利用 QSTileHost 去构建各个开关的视图对象 QSTileView。

编辑页面 QS Tile 的创建

进入 QS 编辑模式之后,会出现更多 Tile,并且可以拖动到上方供用户平时下拉后直接使用,或者将不常用的放到待选区域。这里从UI上可以看出待选区域由两部分组成,一部分是 SystemUI 内部的 Tile(StockTiles),另一部分则是第三应用自己注册的 Tile(PackageTiles)。

1
2
3
4
5
6
7
8
QSPanelController.showEdit()
QsCustomizerController.show()
TileQueryHelper.queryTiles()
TileQueryHelper.addCurrentAndStockTiles()
getString(R.string.quick_settings_tiles_stock)
QSTileHost.createTile()
QSFactoryImpl.createTile()
QSFactoryImpl.createTileInternal()

获取 R.string.quick_settings_tiles_stock 对应的就是SystemUI中可加载的所有Tile,如果希望客制化也可以在此处进行修改。

接下来,再看一下如何加载第三方 Tile。

1
2
3
4
5
6
7
TileQueryHelper.TileCollector.onStateChanged()
TileQueryHelper.TileCollector.finished()
TileAdapter.onTilesChanged()
TileAdapter.recalcSpecs()
TileQueryHelper.addPackageTiles() // 获取带有 TileService.ACTION_QS_TILE 的应用信息
TileQueryHelper.createStateAndAddTile()
TileQueryHelper.addTile()

关联 QS Tile 和 QS View

它们关联的建立主要时通过 QSTile.Callback 和 QSTile.State 来实现。

1
2
3
4
5
6
7
8
public interface Callback {
public static final int VERSION = 1;
void onStateChanged(State state);
void onShowDetail(boolean show);
void onToggleStateChanged(boolean state);
void onScanStateChanged(boolean state);
void onAnnouncementRequested(CharSequence announcement);
}

在 QSPanelControllerBase.addTile() 中创建 QSTileView 时调用 QSPanel.addTile() 为它们建立联系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// QSPanel.java
void addTile(QSPanelControllerBase.TileRecord tileRecord) {
final QSTile.Callback callback = new QSTile.Callback() {
@Override
public void onStateChanged(QSTile.State state) {
drawTile(tileRecord, state);
}

@Override
public void onShowDetail(boolean show) {
// Both the collapsed and full QS panels get this callback, this check determines
// which one should handle showing the detail.
if (shouldShowDetail()) {
QSPanel.this.showDetail(show, tileRecord);
}
}

@Override
public void onToggleStateChanged(boolean state) {
if (mDetailRecord == tileRecord) {
fireToggleStateChanged(state);
}
}

@Override
public void onScanStateChanged(boolean state) {
tileRecord.scanState = state;
if (mDetailRecord == tileRecord) {
fireScanStateChanged(tileRecord.scanState);
}
}

@Override
public void onAnnouncementRequested(CharSequence announcement) {
if (announcement != null) {
mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement)
.sendToTarget();
}
}
};

tileRecord.tile.addCallback(callback); // 向QSTile注册回调
tileRecord.callback = callback;
tileRecord.tileView.init(tileRecord.tile); // 初始化点击事件
tileRecord.tile.refreshState();

if (mTileLayout != null) {
mTileLayout.addTile(tileRecord);
}
}

下面一次点击事件来介绍一下它们之间的关联。
BluetoothTile 继承自 QSTileImpl,实现了抽象方法 handleClick(),handleUpdateState()
点击事件触发后,先把 State 发送给 QSTileImpl 来更新逻辑,然后再通过回调函数通知 QSTileView 更新 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QSTileViewImpl.onClick() // 在init()方法中注册
QSTileImpl.click()
Handler.obtainMessage(H.CLICK, view).sendToTarget()
QSTileImpl.H.handleMessage()
BluetoothTile.handleClick()
QSTileImpl.refreshState()
Handler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget()
QSTileImpl.H.handleMessage()
QSTileImpl.handleRefreshState()
BluetoothTile.handleUpdateState()
BooleanState.value // 更新State
QSTileImpl.handleStateChanged()
QSTile.Callback().onStateChanged(mState) // 在 QSPanel.addTile() 中注册
QSPanel.drawTile()
QSTileViewImpl.onStateChanged()
QSTileViewImpl.handleStateChanged() // 更新 UI
BluetoothController.setBluetoothEnabled() // 更新蓝牙状态